/**
 * @file TEST_timy_shell.c
 * @author Linquan 
 * @brief 	
 * 	一种单片机使用的 简易命令行工具
 * 		* 支持 自定义命令添加
 * 		* 支持 命令输入时，删除错误输入并重新输入
 * 		* 暂不支持 历史输入记录。
 * 		* 命令、参数之间使用空格分隔，命令中不可以使用空格。
 * 		* 输入命令，使用回车表示命令结束。
 * 
 * 		* 默认 命令：
 * 			1、HELP：打印支持的命令种类和使用方法
 * 			2、RB： 开启关闭输入回显。单片机（ARMCC）中默认开启回显
 * 			3、DBON、DBOFF：打开或关闭 LOG_INFO 宏所支持的log类型。
 * 				单独输入DBON\DBOFF，可开启或关闭全部类型的log打印
 * 				DBON\DBOFF 后如果加入 类型索引，可开启指定类型的log打印
 * 				支持 一次输入多个索引，索引之间使用 空格分隔。
 * 		
 * 	移植说明：
 * 		1、准备好一个 可以收发数据的串口工程。
 * 		2、将本文件及头文件添加到工程。
 * 		3、将 tiny_shell_save_data 函数在 串口接收中断调用，将串口接收的数据传入。
 * 		4、将 tiny_shell_anayly 函数在 主循环中调用。
 * 		即可移植完成。
 * 
 * 	自定义命令：
 * 		1、使用 int(*cmd_func_t)(int argc, char**argv) 进行命令函数自定义
 * 		2、命令函数的命名 依据 cmd_xxxx 的格式进行。
 * 		3、命令函数的传入参数 argc 表示 本次命令输入的 参数的总个数（包含命令）。
 * 		4、命令函数的传入参数 argv 表示 全部的传入参数，可以使用数组下标的方式进入读取。
 * 			将第一个参数 命令， 后续参数 认为是 命令附带的参数。
 * 			这些参数是 字符串，需要使用转换函数，转换为数值类型。
 * 		5、可依照 cmd_test 命令函数进行编写 自定义命令函数。
 * 		6、编写完成后，将命令函数添加到 命令表 g_my_cmds 中。
 * 			* 添加的命令 使用 命令函数名 cmd_xxxx 中的 xxxx表示
 * 			！！！要求 命令全部使用大写字母，并不能带有空格，单词间分割可使用下划线。
 * 			
 * @version 0.1
 * @date 2024-12-07
 * 
 * @copyright Copyright (c) 2024
 * 
 * @history:
 * 	2025-3-31:
 * 		1、将中断中接受的数据，移动到主循环中进行解析处理，提高中断接受数据的速度，不丢包。
 *		2、增加log 打印功能，支持最大32种类型打印，可选择全部打印或打印部分类型log。
 *		3、优化help命令说明的打印格式。
 *	2025-4-3:
 *		1、优化log功能，当前可支持打开或关闭任意个log类型的打印。
 *		2、增加log打印传入参数校验功能，传入超过32的索引进行报错提示。
 *		3、log打印宏优化，当不使用printf函数打印时，可以打开 NOT_USE_printf_function 宏
 *			通过宏传入想调用的打印函数。
 * 
 *
 *
 */

#include "tiny_shell.h"

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>


/*************** 类型定义 *********************************/
#define LINE_BUF_MAX_LEN	(64)
char g_line_buf[LINE_BUF_MAX_LEN+1] = {0};

#define RB_ON	1
#define RB_OFF  0
#ifdef ARMCC_VERSION 
    char read_back_flag = 1;  /**> 单片机中，默认开启回显 */
#elif defined (__GNUC__)
   char read_back_flag = 0;  /**> linux中，默认关闭回显 */
#else   
      #error "please choose whether open read back!!!"
#endif

/* 定义命令结构体 */
typedef struct 
{
	char* name;
	cmd_func_t func;
	char* help;
}cmd_t;

/* 数据接收缓冲区 */
struct tiny_shell_fifo
{
	char temp_recv[LINE_BUF_MAX_LEN];
	unsigned int in_ptr;
	unsigned int out_ptr;
}tiny_shell_recv_buff;

/******************************命令列表****************************************/

/**> 命令执行函数声明 */
int cmd_test(int argc, char**argv);

int cmd_help(int argc, char**argv);
int cmd_read_back(int argc, char**argv);
int cmd_debug_switch(int argc, char**argv);

/**> 命令表
 * 用空格分隔命令和参数，所以命令名称不能用空格 
 * */
cmd_t g_my_cmds[] = 
{
	{"HELP", 		cmd_help, 			"Show this list"},
    {"RB", 			cmd_read_back, 		"input\"ON\" or\"OFF\" to open or close read back"},
	{"DBON", 		cmd_debug_switch,	"input \"DBON\" without para,open all log print,\r\n "
										"\t\tinput with log type para,open this log print type"
										LOG_TYPE_BRIF},
	{"DBOFF", 		cmd_debug_switch,	"input \"DBOFF\" without para,close all log print,\n"\
										"\t\tinput with log type para,open this log print type" \
										LOG_TYPE_BRIF},
	{"test", 		cmd_test, 			"Show all params"},
};


/******************************命令列表 end ***************************************/

/**********************************
	数据接收缓冲区操作函数
*************************************/
/**
 * @brief 向缓冲区填入一个数据
 * 
 * @param recv_data 
 */
static void recv_buff_in(char recv_data)
{
	tiny_shell_recv_buff.temp_recv[tiny_shell_recv_buff.in_ptr] = recv_data;
	tiny_shell_recv_buff.in_ptr = (tiny_shell_recv_buff.in_ptr + 1) % LINE_BUF_MAX_LEN;
}

/**
 * @brief 从缓冲区取出一个数据
 * 
 * @return char 
 */
static char  recv_buff_out()
{
	char data = tiny_shell_recv_buff.temp_recv[tiny_shell_recv_buff.out_ptr];
	tiny_shell_recv_buff.out_ptr = (tiny_shell_recv_buff.out_ptr + 1) % LINE_BUF_MAX_LEN;
	
	return data;
}

/**
 * @brief 获取当前缓冲区中未处理的数据
 * 
 * @return char 未处理的数据数量
 */
static int recv_buff_used_number()
{
	return (tiny_shell_recv_buff.in_ptr - tiny_shell_recv_buff.out_ptr);
}


/**********************默认命令功能函数定义******************************* */

int cmd_test(int argc, char**argv)
{
	int i = 0;
	printf("argc=%d ", argc);
	printf("argv=[");
	for(i=0; i<(argc-1); i++)
	{
		printf("%s,", argv[i]);
	}
	printf("%s]\r\n", argv[i]);
}


/**
 * @brief 帮助菜单
 * 
 */
int g_num_cmd = sizeof(g_my_cmds)/sizeof(cmd_t);

int cmd_help(int argc, char**argv)
{
    printf("commacd:  Instructions \r\n");

	for(int i=0; i<g_num_cmd; i++)
	{   
        /**> 左对齐10个字符 */
		printf("%-10s: \n\t\t%s\r\n", g_my_cmds[i].name, g_my_cmds[i].help);
	}
}

/**
 * @brief 回显设置，
 * 		某些命令行软件没有输入内容返回显示，可以通过该命令启动或关闭回显
 * 
 */
int cmd_read_back(int argc, char**argv)
{
    if(argc > 1)
    {
        if(strcmp(argv[1], "OFF") == 0)
        {
            read_back_flag = RB_OFF;
            printf("read back is close ,current read backe flag is %d \r\n ",read_back_flag);
        }
        else if(strcmp(argv[1], "ON") == 0)
        {
            read_back_flag = RB_ON;
            printf("read back is open ,current read backe flag is %d \r\n ",read_back_flag);
        }
        else
        {
            printf("read back para error,para is %s\n ", argv[1] );
        }
    }
    else
    {
        printf("there is no para.\r\nplease input 1->open or 0->close\n ");
    }
}


/**
 * @brief  log 分类打印控制
 * 
 */

/* log 打印开关,
	最大支持 32种类型的打印，打印默认全部关闭 
*/
#define LOG_ALL_CLOSE	0
#define LOG_ALL_OPEN	0XFFFFFFFF
#define MAX_LOG_TYPE	(sizeof(log_print_switch)*8)
uint32_t log_print_switch = LOG_ALL_CLOSE; 


/**
	单独输入 DBON DBOFF 是开启全部打印，
	在后边加入 log_print_type_t 类型对应的序号用于启动不同的打印信息
*/
int cmd_debug_switch(int argc, char**argv)
{
	uint8_t print_type; 

	if(strcmp(argv[0], "DBON") == 0)
	{
		if(argc > 1)
		{
			for(int index = 1; index < argc ; index++)
			{
				print_type = atoi(argv[index]);

				if(print_type < MAX_LOG_TYPE )
				{
					log_print_switch |= (1<<print_type);			
					printf("type %d log print is open\r\n", print_type);	
				}
				else
				{
					printf("type %d index error\r\n", print_type);
				}					
			}
		}		
		else
		{						
			log_print_switch = LOG_ALL_OPEN;
			
			printf("log print is ALL open! \r\n ");
		}				
	}
	else if(strcmp(argv[0], "DBOFF") == 0)
	{
		if(argc > 1)
		{
			for(int index = 1; index < argc ; index++)
			{
				print_type = atoi(argv[index]);	
		
				if(print_type < MAX_LOG_TYPE)
				{
					log_print_switch &= ~(1<<print_type);		
					printf("type %d log print is close  \r\n ", print_type);		
				}
				else
				{
					printf("type %d index error \r\n ", print_type);
				}
			}
		}		
		else
		{			
			log_print_switch = LOG_ALL_CLOSE;
			printf("log print is ALL close! \r\n ");
		}				
	}
	else
	{
	   printf("error para %s\n ", argv[0] );
	}
   
	return 0;
}

/******************************默认命令功能函数定义 end****************************************/


/******************************命令行输入解析****************************************/

/**
 * @brief Get the param object
 * 		以delim为隔断 分割msg字符串，将分割后的每一段放在 get[]中
 * @param msg  需要分割的字符串
 * @param delim 分割的标志
 * @param get 	分割后的字符串
 * @param max_num  支持最大分割段数
 * @return int 
 */
static int get_param(char* msg, char*delim, char* get[], int max_num)
{
	int i,ret;
	char *ptr = NULL;
	ptr = strtok(msg, delim);
	for(i=0; ptr!=NULL &&i<max_num; i++)
	{
		get[i] = ptr;
		ptr = strtok(NULL, delim);
	}
	ret = i;
	return ret;
}

/*
*用于在接受的数据中 找到一条以回车换行结尾的命令
*/
uint8_t get_line(char rcv_char, char *line, uint8_t maxLen)
{
	static uint16_t count = 0; /*用于记录除特殊字符外的其他有效字符的数量*/
		
	if (count >= maxLen) /*长度超限*/
	{
		count = 0; /*清零计数器以便后续使用*/
		return 1;  /*返回有效标志*/
	}

	line[count] = rcv_char; /*记录数据*/

	switch (rcv_char)
	{
		case 0x08:
		case 0x7F: /*退格键或者删除键*/
		{
			if (count > 0)
			{
				count--; /*删除上一个接收到的字符*/
			}
		}
		break;

		case '\r':
		case '\n': /*接收到回车换行，证明已经收到一个完整的命令*/
		{
			line[count] = '\0'; /*添加字符串结束符，刚好可以去掉'\r'或者'\n'*/
			count = 0;			/*清零计数器以便后续使用*/
			return 1;			/*返回有效标志*/
		}
		break;

		default:
			count++;
	}
	
	if(read_back_flag)
	{
		printf("%c", rcv_char); /*把收到的字符输出到串口*/
	}
	
	return 0;
}

/**
 * @brief 解析搜索命令，并执行对应命令函数
 * 
 * @param argc 传入参数的个数，包括命令、命令附带的参数
 * @param argv 每个传入参数，命令为第一个参数
 * @return int 
 */
int match_cmd(int argc, char**argv)
{
	int i = 0;
	for(i = 0; i < g_num_cmd; i++)
	{
		if(strcmp(argv[0], g_my_cmds[i].name) == 0)
		{
			g_my_cmds[i].func(argc, argv);

			return 1;
		}
	}
	
	if(i == g_num_cmd)
	{
		printf("cmd \"%s\" does not exist!!\r\n", argv[0]);
	}
	return 0;
}


/**
 * @brief 处理传入外部输入的字符，并进行命令匹配
 * 			用于单片机时，在串口中断中调用该函数即可
 * @param rcv_char 外边输入的字符
 * 
 */
void shell_cmd_anaylise_char(char rcv_char)
{
	if(get_line(rcv_char, g_line_buf, LINE_BUF_MAX_LEN))
	{
        /**> 回显换行 */
        if(read_back_flag)
        {
            printf("\r\n"); 
        }

		/* 获得到指令 */
		if(strlen(g_line_buf)) /* 判断接受到的指令字符串是否为0 */
		{
			char *argv[16];

			// 用空格将输入的数据分开，保存到 argv中，返回分隔的参数数量
			int argc = get_param(g_line_buf, " ", argv, 16);
			
			// 匹配命令
			match_cmd(argc, argv);			
		}
		printf("->");
	}
}

/******************************命令行输入解析 end****************************************/


/****************************** 对外 API ****************************************/

/******
	call in usart's interrrupt
	write one data to recv buffer 
**/

void tiny_shell_save_data(char recv_data)
{
	recv_buff_in(recv_data);
}
	


/***
	call in main cycle
	get several data from recv buffer ,and anaylise by get_line
*/

void tiny_shell_anayly()
{
	char rcv_char;
	
	while(recv_buff_used_number())
	{
		/* 取出一个数据 */
		rcv_char = recv_buff_out();
		
		/* 送入解析 */
		shell_cmd_anaylise_char(rcv_char);	
	}
}

/****************************** 对外 API end ****************************************/
